use std::{
    path::{Path, PathBuf},
    sync::mpsc::{self, Receiver, Sender, TryRecvError},
    time::Duration,
};

use anyhow::{Context, ensure};
use log::{debug, trace, warn};
pub use notify::{Event, EventKind};
use notify::{RecommendedWatcher, RecursiveMode, Watcher};

use crate::err::AnyResult;

#[derive(Debug)]
pub struct FileWatcher {
    _watcher: RecommendedWatcher,
    handle: Option<std::thread::JoinHandle<()>>,
    drop_tx: Sender<()>,
}

impl FileWatcher {
    pub fn new<P, F>(path: P, mut op: F) -> AnyResult<Self>
    where
        P: AsRef<Path> + Into<PathBuf> + std::fmt::Debug,
        F: FnMut(Event) + Send + 'static,
    {
        let dir = path
            .as_ref()
            .parent()
            .with_context(|| format!("parent not found, path={path:?}"))?;
        let dir_exists = dir.try_exists().context("check watching dir failed")?;

        ensure!(dir_exists, "watching dir not found");
        let filename = path
            .as_ref()
            .file_name()
            .with_context(|| format!("unable to get filename, path={path:?}"))?
            .to_owned();

        let (tx, rx) = mpsc::channel::<Event>();
        let (drop_tx, drop_rx) = mpsc::channel::<()>();
        let mut watcher =
            notify::recommended_watcher(move |res: notify::Result<Event>| match res {
                Ok(ev) => {
                    if matches!(
                        &ev.kind,
                        EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)
                    ) && ev.paths.iter().any(|p| p.file_name() == Some(&filename))
                    {
                        debug!("watching file event detected, event={ev:?}");
                        tx.send(ev).ok();
                    }
                }
                Err(e) => {
                    warn!("wathcing file failed, {e}");
                }
            })?;

        watcher
            .watch(dir, RecursiveMode::NonRecursive)
            .with_context(|| format!("watching dir failed, path={dir:?}"))?;

        debug!("started watching file, path={path:?}");
        let path = path.into();
        let handle = std::thread::spawn(move || {
            trace!("file watcher thread started ok, path={path:?}");
            Self::recv_loop(rx, drop_rx, &mut op);
            debug!("file watcher thread stopping ... path={path:?}");
        });

        Ok(Self {
            _watcher: watcher,
            handle: Some(handle),
            drop_tx,
        })
    }

    fn recv_loop<F>(rx: Receiver<Event>, drop_rx: Receiver<()>, op: &mut F)
    where
        F: FnMut(Event),
    {
        loop {
            if let Ok(_) | Err(TryRecvError::Disconnected) = drop_rx.try_recv() {
                debug!("file watcher shutdown signal received, exiting ...");
                break;
            }

            if let Ok(ev) = rx.recv_timeout(Duration::from_millis(200)) {
                debug!("received first file signal, event={ev:?}");
                while rx.try_recv().is_ok() {
                    trace!("debouncing: consuming subsequent event signal");
                }

                trace!("debouncing period ended, triggering callback operation");
                op(ev);
            }
        }
    }
}

impl Drop for FileWatcher {
    fn drop(&mut self) {
        trace!("file watcher is being dropped, cleaning up...");
        self.drop_tx.send(()).ok();
        if let Some(handle) = self.handle.take() {
            if handle.join().is_err() {
                warn!("file watcher thread panicked on shutdown");
            } else {
                debug!("file watcher thread joined ok");
            }
        }
    }
}
